<?php
    abstract class Form
    {
        const ERROR_LEVEL_ERROR   = 0;
        const ERROR_LEVEL_WARNING = 1;

        const CSRF_PROTECTION = 0;
        const CAPTCHA_PROTECTION = 1;

        const ERROR_TEMPLATES_PATH = "include/errors/";
        const ERROR_MESSAGE_TEMPLATE = "error_message.tpl";
        const ERROR_TEMPLATE = "error.tpl";

        protected static $csrfSecret = 'q3aUVB';
        protected static $csrfFieldName = 'csrf';

        protected $csrfProtection = false;

        protected static $captchaFieldName = 'captcha';
        protected static $captchaInvalidMessage = '<strong>Private code: </strong>The characters you entered don\'t match the image. Please try again';

        protected $protection = false;

        protected $enableCloseFeedback = true;

        /**
         * @var TemplateEngine
         */
        protected $tmpl;

        /**
         *
         * @var array
         */
        protected $fields;

        /**
         *
         * @var array
         */
        protected $defaults;

        /**
         * Contains validated form fields values
         *
         * @var array
         */
        protected $values;

        /**
         * Contains uploaded files
         *
         * @var array
         */
        protected $files;

        /**
         * @var array
         */
        protected $errors;

        /**
         * @var array
         */
        protected $globalErrors;

        /**
         *
         * @var string
         */
        protected $language = 'en';


        /**
         * Indicates if form fields is bound to request params
         *
         * @var boolean
         */
        private $isBound;


        /**
         *
         * @param int $protection Form protection method (csrf or captcha).
         *
         */
        public function __construct( $protection = false )
        {
            $this->resetFields();

            if ( $protection === self::CSRF_PROTECTION
                || $protection === self::CAPTCHA_PROTECTION )
            {
                $this->protection = $protection;
            }

            $this->tmpl = Core::CreateTemplateEngine( PATH_TEMPLATES );
            $this->tmpl->define( 'form', $this->GetTemplatePath() );

            $this->Setup();
        }

        public function ResetFields()
        {
            $this->errors = array();
            $this->fields = array();
            $this->values = array();
            $this->defaults = array();
        }

        public function SetLanguage( $language )
        {
            $this->language = $language;
        }

        public function GetLanguage()
        {
            return $this->language;
        }

        public function SetEnableCloseFeedback( $enableCloseFeedback )
        {
            $this->enableCloseFeedback = $enableCloseFeedback;
        }

        public function GetEnableCloseFeedback()
        {
            return $this->enableCloseFeedback;
        }

        /**
         * Prepares form data.
         */
        protected function PrepareFormData()
        {
        }

        abstract protected function GetTemplatePath();

        /**
         * Setup form fields
         */
        abstract function Setup();

        /**
         * Binds validated request param values to form fields
         *
         */
        public function Bind()
        {
            $this->isBound = true;

            $this->errors = array();

            // reset form values
            $this->values = array();
            $this->files = array();

            foreach ($_FILES as $fieldName => $file)
            {
                $this->files[$fieldName] = $file;
            }
            $this->Validate();
        }

        /**
         * @return bool
         */
        public function HasErrors()
        {
            return (bool) count($this->errors);
        }

        /**
         * @return bool
         */
        public function IsValid()
        {
            if (!$this->isBound)
            {
                return false;
            }

            return !$this->HasErrors();
        }

        /**
         * @return bool
         */
        public function IsBound()
        {
            return $this->isBound;
        }

        public function GetCSRFToken()
        {
            return md5(self::$csrfSecret.get_class($this));
        }

        /**
         * Sets field error.
         *
         * @param string $fieldName Field name where error occurred
         * @param mixed  $error Error message string or an array
         *      ('message' => errorMessage, 'level' => errorLevel, 'code' => errorCode)
         */
        public function SetError($fieldName, $error, $code = null)
        {
            if (!is_array($error))
            {
                $error = array('message' => $error, 'level' => self::ERROR_LEVEL_ERROR, 'code' => $code);
            }

            if (!is_null($fieldName))
            {
                $this->errors[$fieldName] = $error;
            }
            else
            {
                $this->errors[] = $error;
            }
        }

        /**
         * Get fields errors
         */
        public function GetErrors()
        {
            return $this->errors;
        }

        /**
         * Sets form field value.
         *
         * @param string $fieldName
         * @param string $value
         */
        public function SetValue( $fieldName, $value )
        {
            $this->values[ $fieldName ] = $value;
        }

        public function SetDefaultValue( $fieldName, $value )
        {
            $this->defaults[ $fieldName ] = $value;
        }

        /**
         * Gets the form field value.
         *
         * @param  string $fieldName
         * @param  mixed  $defaultValue
         * @return mixed  Field value or defaultValue if form is not bound.
         */
        public function GetValue( $fieldName, $defaultValue = null )
        {
            return $this->IsBound() && isset($this->values[$fieldName]) ? $this->values[$fieldName] : $defaultValue;
        }

        /**
         * Gets the form field default value.
         *
         * @param  string $fieldName
         * @return mixed  Field default value or null if not configured.
         */
        public function GetDefaultValue( $fieldName )
        {
            return isset($this->defaults[$fieldName]) ? $this->defaults[$fieldName] : null;
        }

        /**
         * Returns param value.
         *
         * @param  string $name
         * @return mixed
         */
        public function GetParamValue( $name, $defaultValue = null )
        {
            return Request::GetParam( $name, $defaultValue );
        }

        /**
         * Returns file
         *
         * @param string $fieldName
         */
        public function GetFile( $fieldName )
        {
           return $this->isBound && isset($this->files[$fieldName]) ? $this->files[$fieldName] : null;
        }

        public function GetFiles()
        {
            $files = null;

            $file = $this->GetFile('Filedata');
            if ($file)
            {
                if ($file['error'] == UPLOAD_ERR_OK)
                {
                    $content = @file_get_contents($file['tmp_name']);
                    if ($content !== false)
                    {
                        $files = array( array(
                            'name' => $file['name'],
                            'type' => $file['type'],
                            'content' => $content
                        ));
                    }
                }
            }

            return $files;
        }

        /**
         * Gets all values
         *
         * @return array
         */
        public function GetValues()
        {
            return $this->isBound ? $this->values : array();
        }

        /**
         * Generates html for form
         *
         * @return string html
         */
        public function Generate( $isUsingVars = false )
        {
            $tmpl = $this->tmpl;
            if ( !$isUsingVars )
            {
                $tmpl->clearVars();
            }

            $this->PreGenerate();

            $this->SetFormTemplateValues();
            $this->SetFormTemplateErrors();

            $tmpl->parse('content', '.form');
            $html = $tmpl->getGenerated();

            return $html;
        }

        /**
         *
         * @param array $fields
         */
        protected function SetFields( $fields )
        {
            $this->resetFields();

            $this->fields = is_array($fields) ? $fields : array($fields);
            $this->AddCSRFToken();
        }

        private function AddCSRFToken()
        {
            array_unshift($this->fields, self::$csrfFieldName);
            $this->SetDefaultValue(self::$csrfFieldName, $this->GetCSRFToken());
        }

        /**
         * Is executed before html is generated.
         * Override to set up some custom template
         * variables
         */
        protected function PreGenerate()
        {
        }

        /**
         * Validates submitted field values
         */
        protected function Validate()
        {
            if (!$this->IsBound())
            {
                return;
            }

            if ($this->protection === self::CSRF_PROTECTION)
            {
                // Verify CSRF token
                if ( $this->GetCSRFToken() != $this->GetParamValue(self::$csrfFieldName) )
                {
                    Util::Redirect('error404.html');
                }

                $this->SetValue(self::$csrfFieldName, $this->GetParamValue(self::$csrfFieldName));
            }
            else if ($this->protection === self::CAPTCHA_PROTECTION)
            {
                // Verify checkbox captcha turned on
                if ( !$this->GetParamValue(self::$captchaFieldName) )
                {
                    $this->SetError(self::$captchaFieldName, self::$captchaInvalidMessage);
                }
            }

            $this->PrepareFormData();
        }

        /**
         * Sets form fields values.
         */
        protected function SetFormTemplateValues()
        {
            foreach ($this->fields as $fieldName)
            {
                $fieldValue = $this->isBound ? $this->GetParamValue($fieldName) : $this->GetDefaultValue($fieldName);

                // todo: find correct way to process such cases
                if (is_array($fieldValue))
                {
                    $fieldValue = implode(', ', $fieldValue);
                }

                $this->tmpl->set( strtoupper($fieldName) . '_VALUE', $fieldValue, true );
            }
        }

        /**
         * Return form errors block.
         */
        public function GetFormTemplateErrors()
        {
            $errorMessageVars = TemplateLanguage::GetTemplateLngVars(
                self::ERROR_TEMPLATES_PATH, $this->language );
            $intro = Util::GetArrayCorrectString( $errorMessageVars, "INTRO" );

            $errorsTmpl = Core::CreateTemplateEngine( PATH_TEMPLATES . self::ERROR_TEMPLATES_PATH );
            $errorList = $this->SetErrorList($errorsTmpl);

            $errorsTmpl->clearAll();
            $errorsTmpl->define('error_message', self::ERROR_MESSAGE_TEMPLATE);
            $errorsTmpl->set('ENABLE_CLOSE', $this->enableCloseFeedback);
            if ($this->IsShowErrorIntro())
            {
                $errorsTmpl->set('INTRO', $intro);
            }
            $errorsTmpl->set('ERRORS', $errorList);
            $errorsTmpl->parse('ERROR_MESSAGE', 'error_message');

            return $errorsTmpl->getGenerated();
        }

        /**
         * Sets form errors block.
         */
        protected function SetFormTemplateErrors()
        {
            $this->tmpl->set('ERROR_MESSAGE', $this->GetFormTemplateErrors());
        }

        /**
         * Sets error list
         * @param TemplateEngine $tpl
         * @return string $result
         *
         */
        protected function SetErrorList($tmpl)
        {
            $result = '';
            foreach ($this->errors as $fieldName => $error)
            {
                $tmpl->clearAll();
                $tmpl->define('error', self::ERROR_TEMPLATE);
                $tmpl->set('ERROR', $error["message"]);
                if (isset($error["field_name"]))
                {
                    $tmpl->set('FIELD_NAME', $error["field_name"]);
                }
                $tmpl->set('ERROR_NAME_LABEL', $fieldName);
                $tmpl->parse('ERROR', 'error');
                $result .= $tmpl->getGenerated();
            }
            return $result;
        }

        /**
         * Checks errors in fields, if there is a display intro.
         */
        protected function IsShowErrorIntro()
        {
            $isShow = (count($this->errors) == 0);
            foreach ( $this->errors as $error )
            {
                if (!empty($error["field_name"]))
                {
                    $isShow = true;
                }
            }

            return $isShow;
        }

        /**
         * Set vars for class template engine.
         *
         * @param array $vars
         */
        public function SetVars( $vars )
        {
            if ( empty( $vars ) )
            {
                return;
            }

            foreach ( $vars as $key => $value )
            {
                $this->tmpl->set( $key, $value );
            }
        }

        /**
         * Set var for class template engine.
         *
         * @param array $vars
         */
        public function SetVar( $var, $value )
        {
            if ( empty( $var ) )
            {
                return;
            }
            $this->tmpl->set( $var, $value );
        }

        public function SetArrVar( $var, $value )
        {
            if ( empty( $var ) )
            {
                return;
            }
            $this->tmpl->setArr( $var, $value );
        }

        protected function GetFieldValue( $fieldName )
        {
            return preg_replace( '/\s+/', ' ', $this->GetParamValue( $fieldName, '' ) );
        }

    }
?>